/*
 * Copyright 2016 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

package io.netty.util;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import static io.netty.util.internal.ObjectUtil.checkNotNull;

/**
 * Builder for immutable {@link DomainNameMapping} instances.
 *
 * @param <V> concrete type of value objects
 */
public final class DomainNameMappingBuilder<V> {

  private final V defaultValue;
  private final Map<String, V> map;

  /**
   * Constructor with default initial capacity of the map holding the mappings
   *
   * @param defaultValue the default value for {@link DomainNameMapping#map(String)} to return when
   * nothing matches the input
   */
  public DomainNameMappingBuilder(V defaultValue) {
    this(4, defaultValue);
  }

  /**
   * Constructor with initial capacity of the map holding the mappings
   *
   * @param initialCapacity initial capacity for the internal map
   * @param defaultValue the default value for {@link DomainNameMapping#map(String)} to return when
   * nothing matches the input
   */
  public DomainNameMappingBuilder(int initialCapacity, V defaultValue) {
    this.defaultValue = checkNotNull(defaultValue, "defaultValue");
    map = new LinkedHashMap<String, V>(initialCapacity);
  }

  /**
   * Adds a mapping that maps the specified (optionally wildcard) host name to the specified output
   * value. Null values are forbidden for both hostnames and values.
   * <p>
   * <a href="http://en.wikipedia.org/wiki/Wildcard_DNS_record">DNS wildcard</a> is supported as
   * hostname. For example, you can use {@code *.netty.io} to match {@code netty.io} and {@code
   * downloads.netty.io}.
   * </p>
   *
   * @param hostname the host name (optionally wildcard)
   * @param output the output value that will be returned by {@link DomainNameMapping#map(String)}
   * when the specified host name matches the specified input host name
   */
  public DomainNameMappingBuilder<V> add(String hostname, V output) {
    map.put(checkNotNull(hostname, "hostname"), checkNotNull(output, "output"));
    return this;
  }

  /**
   * Creates a new instance of immutable {@link DomainNameMapping} Attempts to add new mappings to
   * the result object will cause {@link UnsupportedOperationException} to be thrown
   *
   * @return new {@link DomainNameMapping} instance
   */
  public DomainNameMapping<V> build() {
    return new ImmutableDomainNameMapping<V>(defaultValue, map);
  }

  /**
   * Immutable mapping from domain name pattern to its associated value object. Mapping is
   * represented by two arrays: keys and values. Key domainNamePatterns[i] is associated with
   * values[i].
   *
   * @param <V> concrete type of value objects
   */
  private static final class ImmutableDomainNameMapping<V> extends DomainNameMapping<V> {

    private static final String REPR_HEADER = "ImmutableDomainNameMapping(default: ";
    private static final String REPR_MAP_OPENING = ", map: {";
    private static final String REPR_MAP_CLOSING = "})";
    private static final int REPR_CONST_PART_LENGTH =
        REPR_HEADER.length() + REPR_MAP_OPENING.length() + REPR_MAP_CLOSING.length();

    private final String[] domainNamePatterns;
    private final V[] values;
    private final Map<String, V> map;

    @SuppressWarnings("unchecked")
    private ImmutableDomainNameMapping(V defaultValue, Map<String, V> map) {
      super(null, defaultValue);

      Set<Map.Entry<String, V>> mappings = map.entrySet();
      int numberOfMappings = mappings.size();
      domainNamePatterns = new String[numberOfMappings];
      values = (V[]) new Object[numberOfMappings];

      final Map<String, V> mapCopy = new LinkedHashMap<String, V>(map.size());
      int index = 0;
      for (Map.Entry<String, V> mapping : mappings) {
        final String hostname = normalizeHostname(mapping.getKey());
        final V value = mapping.getValue();
        domainNamePatterns[index] = hostname;
        values[index] = value;
        mapCopy.put(hostname, value);
        ++index;
      }

      this.map = Collections.unmodifiableMap(mapCopy);
    }

    @Override
    @Deprecated
    public DomainNameMapping<V> add(String hostname, V output) {
      throw new UnsupportedOperationException(
          "Immutable DomainNameMapping does not support modification after initial creation");
    }

    @Override
    public V map(String hostname) {
      if (hostname != null) {
        hostname = normalizeHostname(hostname);

        int length = domainNamePatterns.length;
        for (int index = 0; index < length; ++index) {
          if (matches(domainNamePatterns[index], hostname)) {
            return values[index];
          }
        }
      }

      return defaultValue;
    }

    @Override
    public Map<String, V> asMap() {
      return map;
    }

    @Override
    public String toString() {
      String defaultValueStr = defaultValue.toString();

      int numberOfMappings = domainNamePatterns.length;
      if (numberOfMappings == 0) {
        return REPR_HEADER + defaultValueStr + REPR_MAP_OPENING + REPR_MAP_CLOSING;
      }

      String pattern0 = domainNamePatterns[0];
      String value0 = values[0].toString();
      int oneMappingLength =
          pattern0.length() + value0.length() + 3; // 2 for separator ", " and 1 for '='
      int estimatedBufferSize = estimateBufferSize(defaultValueStr.length(), numberOfMappings,
          oneMappingLength);

      StringBuilder sb = new StringBuilder(estimatedBufferSize)
          .append(REPR_HEADER).append(defaultValueStr).append(REPR_MAP_OPENING);

      appendMapping(sb, pattern0, value0);
      for (int index = 1; index < numberOfMappings; ++index) {
        sb.append(", ");
        appendMapping(sb, index);
      }

      return sb.append(REPR_MAP_CLOSING).toString();
    }

    /**
     * Estimates the length of string representation of the given instance: est =
     * lengthOfConstantComponents + defaultValueLength + (estimatedMappingLength * numOfMappings) *
     * 1.10
     *
     * @param defaultValueLength length of string representation of {@link #defaultValue}
     * @param numberOfMappings number of mappings the given instance holds, e.g. {@link
     * #domainNamePatterns#length}
     * @param estimatedMappingLength estimated size taken by one mapping
     * @return estimated length of string returned by {@link #toString()}
     */
    private static int estimateBufferSize(int defaultValueLength,
        int numberOfMappings,
        int estimatedMappingLength) {
      return REPR_CONST_PART_LENGTH + defaultValueLength
          + (int) (estimatedMappingLength * numberOfMappings * 1.10);
    }

    private StringBuilder appendMapping(StringBuilder sb, int mappingIndex) {
      return appendMapping(sb, domainNamePatterns[mappingIndex], values[mappingIndex].toString());
    }

    private static StringBuilder appendMapping(StringBuilder sb, String domainNamePattern,
        String value) {
      return sb.append(domainNamePattern).append('=').append(value);
    }
  }
}
